package com.yjk1.example.myFirstSofawebBoot.framework;

import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;

/**
 * @author yujiakui
 * @version 1.0
 * Email: jkyu@haiyi-info.com
 * date: 2018/11/13 14:11
 * description：动态mybatis mapper.xml 刷新
 **/
public class DynamicMybatisMapperRefresh {

    /**
     * logger
     */
    private final static Logger LOGGER = LoggerFactory.getLogger(DynamicMybatisMapperRefresh.class);

    /**
     * 记录jar包存在的mapper
     */
    private static final Map<String, List<Resource>> jarMapper = new HashMap<>();
    private SqlSessionFactory sqlSessionFactory;
    private Resource[] mapperLocations;
    private Configuration configuration;
    private ClassLoader classLoader;
    /**
     * xml文件目录
     */
    private Set<String> fileSet = new HashSet<>();

    /**
     * 构造函数
     *
     * @param mapperLocationStrs
     * @param sqlSessionFactory
     */
    public DynamicMybatisMapperRefresh(String[] mapperLocationStrs,
                                       SqlSessionFactory sqlSessionFactory,
                                       ClassLoader classLoader) {
        this.mapperLocations = resolveMapperLocations(mapperLocationStrs,classLoader);
        this.sqlSessionFactory = sqlSessionFactory;
        this.configuration = sqlSessionFactory.getConfiguration();
        this.classLoader = classLoader;
    }

    /**
     * 根据url获取对应的Resource列表
     *
     * @return
     */
    private Resource[] resolveMapperLocations(String[] mapperLocationStrs,ClassLoader classLoader) {
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(classLoader);
        List<Resource> resources = new ArrayList<Resource>();
        if (mapperLocationStrs != null) {
            for (String mapperLocation : mapperLocationStrs) {
                try {
                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
                    resources.addAll(Arrays.asList(mappers));
                } catch (IOException e) {
                    // ignore
                }
            }
        }
        return resources.toArray(new Resource[resources.size()]);
    }


    /**
     * 清除加载的mapper xml文件
     */
    public void clear(){
        handleXmlMapper(true);
    }

    /**
     * 动态扫描对应的mapper.xml文件
     *
     */
    public void scanMapperXml() {
        for (Resource mapperLocation : mapperLocations) {
            try {
                if (ResourceUtils.isJarURL(mapperLocation.getURL())) {
                    String key = new UrlResource(ResourceUtils.extractJarFileURL(mapperLocation.getURL()))
                            .getFile().getPath();
                    fileSet.add(key);
                    if (jarMapper.get(key) != null) {
                        jarMapper.get(key).add(mapperLocation);
                    } else {
                        List<Resource> resourcesList = new ArrayList<>();
                        resourcesList.add(mapperLocation);
                        jarMapper.put(key, resourcesList);
                    }
                } else {
                    fileSet.add(mapperLocation.getFile().getPath());
                }
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }

        handleXmlMapper(false);
    }

    /**
     * 处理xml mapper
     *
     * @param delFlag
     */
    private void handleXmlMapper(boolean delFlag){
        try {
            for (String filePath : fileSet) {
                File file = new File(filePath);
                if (file.isFile()) {
                    List<Resource> removeList = jarMapper.get(filePath);
                    if (removeList != null && !removeList.isEmpty()) {
                        for (Resource resource : removeList) {
                            refresh(resource,delFlag);
                        }
                    } else {
                        refresh(new FileSystemResource(file),delFlag);
                    }
                }
            }
        } catch (Exception ex) {
            LOGGER.error("加载mapperXml文件失败location={},ex={}", mapperLocations, ex);
            throw new RuntimeException(ex);
        }
    }


    /**
     * 刷新mapper
     *
     * @throws Exception
     */
    @SuppressWarnings("rawtypes")
    private void refresh(Resource resource,boolean delFlag) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        this.configuration = sqlSessionFactory.getConfiguration();
        boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class;
        try {
            Field loadedResourcesField = isSupper ? configuration.getClass().getSuperclass().getDeclaredField("loadedResources")
                    : configuration.getClass().getDeclaredField("loadedResources");
            loadedResourcesField.setAccessible(true);
            Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
            XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(),
                    new XMLMapperEntityResolver());
            XNode context = xPathParser.evalNode("/mapper");
            String namespace = context.getStringAttribute("namespace");
            Field field = MapperRegistry.class.getDeclaredField("knownMappers");
            field.setAccessible(true);
            Map mapConfig = (Map) field.get(configuration.getMapperRegistry());
            Resources.setDefaultClassLoader(classLoader);
            mapConfig.remove(Resources.classForName(namespace));
            loadedResourcesSet.remove(resource.toString());
            configuration.getCacheNames().remove(namespace);
            cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace);
            cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace);
            cleanKeyGenerators(context.evalNodes("insert|update"), namespace);
            cleanSqlElement(context.evalNodes("/mapper/sql"), namespace);
            if(!delFlag) {
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(),
                        sqlSessionFactory.getConfiguration(),
                        resource.toString(), sqlSessionFactory.getConfiguration().getSqlFragments());
                xmlMapperBuilder.parse();
            }
            LOGGER.debug("refresh: '" + resource + "', success!");
        } catch (IOException e) {
            LOGGER.error("Refresh IOException :" + e.getMessage());
        } finally {
            ErrorContext.instance().reset();
        }
    }

    /**
     * 清理parameterMap
     *
     * @param list
     * @param namespace
     */
    private void cleanParameterMap(List<XNode> list, String namespace) {
        for (XNode parameterMapNode : list) {
            String id = parameterMapNode.getStringAttribute("id");
            configuration.getParameterMaps().remove(namespace + "." + id);
        }
    }

    /**
     * 清理resultMap
     *
     * @param list
     * @param namespace
     */
    private void cleanResultMap(List<XNode> list, String namespace) {
        for (XNode resultMapNode : list) {
            String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
            configuration.getResultMapNames().remove(id);
            configuration.getResultMapNames().remove(namespace + "." + id);
            clearResultMap(resultMapNode, namespace);
        }
    }

    private void clearResultMap(XNode xNode, String namespace) {
        for (XNode resultChild : xNode.getChildren()) {
            if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName())
                    || "case".equals(resultChild.getName())) {
                if (resultChild.getStringAttribute("select") == null) {
                    configuration.getResultMapNames().remove(
                            resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    configuration.getResultMapNames().remove(
                            namespace + "." + resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) {
                        clearResultMap(resultChild, namespace);
                    }
                }
            }
        }
    }

    /**
     * 清理selectKey
     *
     * @param list
     * @param namespace
     */
    private void cleanKeyGenerators(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
            configuration.getKeyGeneratorNames().remove(namespace + "." + id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
        }
    }

    /**
     * 清理sql节点缓存
     *
     * @param list
     * @param namespace
     */
    private void cleanSqlElement(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getSqlFragments().remove(id);
            configuration.getSqlFragments().remove(namespace + "." + id);
        }
    }
}
